Skip to content

Update TestClustersPlugin to accept system props installedPlugins and security.enabled for convenience installing plugins for integ tests#20393

Open
cwperks wants to merge 6 commits intoopensearch-project:mainfrom
cwperks:test-clusters-security
Open

Update TestClustersPlugin to accept system props installedPlugins and security.enabled for convenience installing plugins for integ tests#20393
cwperks wants to merge 6 commits intoopensearch-project:mainfrom
cwperks:test-clusters-security

Conversation

@cwperks
Copy link
Member

@cwperks cwperks commented Jan 8, 2026

Description

This PR adds support in the TestClustersPlugin to allow plugins utilizing testclusters to specify:

  1. systemProperty "installedPlugins", "['opensearch-job-scheduler']"
  2. systemProperty "security.enabled", "true"

For convenience to install general plugins or to install the security plugin and have it run with the demo security configuration.

Currently, many plugins re-implement similar logic to install plugins and I believe its simpler to have a common core solution that can be used.

This also allows the certs to be read on the test classpath because many plugins require the admin certificates to run requests to the cluster utilizing the admin cert (adminClient)

Related Issues

Resolves opensearch-project/opensearch-plugins#245

Check List

  • Functionality includes testing.
  • API changes companion pull request created, if applicable.
  • Public documentation issue/PR created, if applicable.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

Summary by CodeRabbit

  • New Features
    • TestClustersPlugin now accepts system properties to conveniently install plugins and enable security configuration for integration tests. Automatic security plugin installation is supported when security is enabled.

✏️ Tip: You can customize this high-level summary in your review settings.

Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Craig Perkins <cwperx@amazon.com>
@cwperks cwperks requested a review from a team as a code owner January 8, 2026 19:51
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

The TestClustersPlugin now accepts system properties to automatically configure installed plugins and security for integration tests. It resolves plugins from Maven repositories or local projects, downloads security certificates when needed, and applies security configurations to test cluster nodes.

Changes

Cohort / File(s) Summary
Documentation
CHANGELOG.md
Added changelog entry documenting new system properties installedPlugins and security.enabled support in TestClustersPlugin
Plugin Configuration
buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java
Added configureInstalledPlugins() method to read system properties and auto-configure plugins per cluster; implemented Maven resolution logic for non-local plugins with snapshot handling; added configureSecurityPlugin() to configure TLS/SSL, certificates, admin DN, and audit settings; integrated certificate download from remote source with local caching

Sequence Diagram(s)

sequenceDiagram
    participant TP as TestClustersPlugin
    participant PROJ as Local Project
    participant MVN as Maven Repository
    participant FS as Filesystem
    participant CERT as Remote Cert Server
    participant CLUSTER as Test Cluster

    TP->>TP: Read system properties<br/>(installedPlugins, security.enabled)
    
    loop For each plugin
        alt Local project plugin
            TP->>PROJ: Locate plugin artifact
            PROJ-->>TP: Return plugin path
        else Maven-resolved plugin
            TP->>MVN: Setup repositories<br/>(mavenLocal, snapshots)
            TP->>MVN: Resolve plugin artifact<br/>(with version/snapshot)
            MVN-->>TP: Return plugin artifact
        end
    end
    
    alt Security enabled
        TP->>CERT: Check for certificate files
        FS->>FS: Create security-resources dir
        TP->>CERT: Download missing certs
        CERT-->>FS: Store certificates
        FS-->>TP: Cert paths ready
    end
    
    TP->>CLUSTER: Install plugins
    TP->>CLUSTER: Apply security settings<br/>(TLS, certs, admin DN, audit)
    CLUSTER-->>TP: Configuration complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A plugin configurer, hopping with glee,
Reads system props with security key,
Downloads certificates, resolves Maven's call,
Configures clusters—now safe for all!
No more manual setup to fuss and to strain,
Security testing made simple again! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately and specifically describes the main change: updating TestClustersPlugin to accept two system properties for installing plugins and enabling security in integration tests.
Description check ✅ Passed The pull request description covers the main objectives, related issues, and includes the required checklist items, though testing checkbox remains unchecked.
Linked Issues check ✅ Passed The changes enable security-enabled integration testing by adding system property support for installing plugins and security configuration, directly addressing issue #245's goal of standardizing security-enabled testing across plugins.
Out of Scope Changes check ✅ Passed All changes are directly related to the core objective: TestClustersPlugin enhancements for plugin installation and security configuration, plus corresponding changelog and certificate handling support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Signed-off-by: Craig Perkins <cwperx@amazon.com>
@cwperks
Copy link
Member Author

cwperks commented Jan 8, 2026

Opened a PR in the job-scheduler to show how these changes in core will simplify the job-scheduler repo: opensearch-project/job-scheduler#873

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In
@buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java:
- Around line 153-182: The security plugin configuration is only applied for
Maven-resolved plugins; when a local project is detected via
project.findProject(":plugins:" + pluginName) the configureSecurityPlugin(...)
call is skipped. Fix this by adding the same conditional check used later to
call configureSecurityPlugin(project, cluster) immediately after the local
branch where cluster.plugin("plugins:" + pluginName) is invoked (i.e., inside
the if block that checks project.findProject), so that pluginName containing
"opensearch-security" triggers configureSecurityPlugin for local project plugins
as well.
- Around line 203-222: The certificate download loop in TestClustersPlugin (the
for loop iterating SECURITY_CERT_FILES) is insecure and racy: replace the
on-the-fly download logic with a safe strategy — either bundle certificates as
resources/artifacts or, if remote retrieval is required, implement checksum
verification and an atomic, locked download flow: compute/obtain expected
checksums (per cert name), download to a temp file, verify the checksum, then
atomically move/rename into place, and guard concurrent access with a file lock
(or create-exclusive temp file + Files.move ATOMIC_MOVE) to prevent simultaneous
writers; update the RuntimeException message in the download block to include
the cert name and the underlying exception details to aid debugging.
- Around line 229-232: Replace the cluster.getNodes().forEach(...) call with
cluster.getNodes().all(...) so security certificate files and settings are
applied to nodes added later (e.g., via setNumberOfNodes()); inside the all(...)
lambda, keep the logic that iterates SECURITY_CERT_FILES and calls
node.extraConfigFile(...) and any other per-node security configuration so every
current and future node receives the certificate and security settings.
🧹 Nitpick comments (2)
buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java (2)

199-199: Deprecated API: project.getBuildDir() is deprecated.

Project.getBuildDir() was deprecated in Gradle 7.1. Consider using the layout API for forward compatibility.

♻️ Proposed fix
-        File buildDir = project.getBuildDir();
-        File securityResourcesDir = new File(buildDir, "security-resources");
+        File securityResourcesDir = project.getLayout().getBuildDirectory().dir("security-resources").get().getAsFile();

165-165: Add a comment explaining the version transformation logic.

The version manipulation .replace("-SNAPSHOT", ".0-SNAPSHOT") is unclear. If the security plugin uses a different versioning scheme (e.g., 3.0.0.0-SNAPSHOT vs 3.0.0-SNAPSHOT), please document this for future maintainers.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 733931b and bf72ab7.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: gradle-check
  • GitHub Check: Analyze (java)
  • GitHub Check: precommit (25, macos-15-intel)
  • GitHub Check: precommit (21, macos-15-intel)
  • GitHub Check: assemble (21, ubuntu-latest)
  • GitHub Check: precommit (25, ubuntu-latest)
  • GitHub Check: precommit (21, macos-15)
  • GitHub Check: precommit (21, windows-2025, true)
  • GitHub Check: precommit (25, ubuntu-24.04-arm)
  • GitHub Check: precommit (21, ubuntu-24.04-arm)
  • GitHub Check: precommit (25, windows-latest)
  • GitHub Check: precommit (21, ubuntu-latest)
  • GitHub Check: precommit (21, windows-latest)
  • GitHub Check: precommit (25, macos-15)
  • GitHub Check: assemble (25, windows-latest)
  • GitHub Check: assemble (25, ubuntu-24.04-arm)
  • GitHub Check: assemble (21, windows-latest)
  • GitHub Check: assemble (21, ubuntu-24.04-arm)
  • GitHub Check: assemble (25, ubuntu-latest)
🔇 Additional comments (4)
CHANGELOG.md (1)

13-13: LGTM!

The changelog entry is well-formatted and correctly placed in the "Added" section.

buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java (3)

245-245: Verify YAML formatting for admin_dn setting.

The embedded newline and YAML list syntax ("\n - CN=...") in the value string is unusual. Verify this produces valid OpenSearch configuration when serialized. The typical pattern would be a JSON array format or proper YAML list handling.


38-66: LGTM on imports.

All new imports are used in the added functionality.


121-124: Integration point is well-placed.

The call to configureInstalledPlugins after registering cluster hooks is appropriate. Note the issues flagged in other comments regarding the implementation details.

Comment on lines +140 to +146
if (installedPluginsProp != null) {
try {
plugins.addAll((List<String>) groovy.util.Eval.me(installedPluginsProp));
} catch (Exception e) {
throw new IllegalArgumentException("Invalid installedPlugins format. Expected list like: ['plugin1','plugin2']", e);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Code injection vulnerability via groovy.util.Eval.me().

Using groovy.util.Eval.me() to parse user input is dangerous. An attacker could inject arbitrary Groovy code via the installedPlugins system property, leading to arbitrary code execution during the build.

Consider using a safe JSON parser or simple string parsing instead.

🔒 Proposed fix using simple string parsing
-                if (installedPluginsProp != null) {
-                    try {
-                        plugins.addAll((List<String>) groovy.util.Eval.me(installedPluginsProp));
-                    } catch (Exception e) {
-                        throw new IllegalArgumentException("Invalid installedPlugins format. Expected list like: ['plugin1','plugin2']", e);
-                    }
-                }
+                if (installedPluginsProp != null) {
+                    // Parse simple list format: ['plugin1','plugin2'] or plugin1,plugin2
+                    String cleaned = installedPluginsProp.trim();
+                    if (cleaned.startsWith("[") && cleaned.endsWith("]")) {
+                        cleaned = cleaned.substring(1, cleaned.length() - 1);
+                    }
+                    for (String part : cleaned.split(",")) {
+                        String pluginName = part.trim().replaceAll("^['\"]|['\"]$", "");
+                        if (!pluginName.isEmpty()) {
+                            plugins.add(pluginName);
+                        }
+                    }
+                }

Comment on lines +153 to +182
for (String pluginName : plugins) {
// Check if it's a local project plugin
if (project.findProject(":plugins:" + pluginName) != null) {
cluster.plugin("plugins:" + pluginName);
} else {
// Resolve from Maven
project.getRepositories().mavenLocal();
project.getRepositories().maven(repo -> {
repo.setName("OpenSearch Snapshots");
repo.setUrl("https://ci.opensearch.org/ci/dbc/snapshots/maven/");
});

String version = VersionProperties.getOpenSearch().replace("-SNAPSHOT", ".0-SNAPSHOT");
String coords = pluginName.contains(":") ? pluginName : (pluginName + ":" + version);
if (!coords.contains("@")) {
coords += "@zip";
}
String fullCoords = coords.split(":").length < 3 ? "org.opensearch.plugin:" + coords : coords;

org.gradle.api.artifacts.Configuration config = project.getConfigurations()
.detachedConfiguration(project.getDependencies().create(fullCoords));
config.getResolutionStrategy().cacheChangingModulesFor(0, "seconds");
cluster.plugin(project.getLayout().file(project.provider(() -> config.getSingleFile())));

// Configure security if opensearch-security plugin is installed
if (pluginName.contains("opensearch-security")) {
configureSecurityPlugin(project, cluster);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Security plugin configuration skipped for local project installations.

When opensearch-security is found as a local project (line 155-156), the configureSecurityPlugin() method is never called. This means local builds of the security plugin won't have the necessary TLS/certificate configuration applied, causing inconsistent behavior between local and Maven-resolved plugins.

🐛 Proposed fix
                 for (String pluginName : plugins) {
                     // Check if it's a local project plugin
                     if (project.findProject(":plugins:" + pluginName) != null) {
                         cluster.plugin("plugins:" + pluginName);
+                        // Configure security if opensearch-security plugin is installed locally
+                        if (pluginName.contains("opensearch-security")) {
+                            configureSecurityPlugin(project, cluster);
+                        }
                     } else {
                         // Resolve from Maven
                         // ... existing code ...
                     }
                 }
🤖 Prompt for AI Agents
In
@buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java
around lines 153 - 182, The security plugin configuration is only applied for
Maven-resolved plugins; when a local project is detected via
project.findProject(":plugins:" + pluginName) the configureSecurityPlugin(...)
call is skipped. Fix this by adding the same conditional check used later to
call configureSecurityPlugin(project, cluster) immediately after the local
branch where cluster.plugin("plugins:" + pluginName) is invoked (i.e., inside
the if block that checks project.findProject), so that pluginName containing
"opensearch-security" triggers configureSecurityPlugin for local project plugins
as well.

Comment on lines +203 to +222
for (String certFile : SECURITY_CERT_FILES) {
File localFile = new File(securityResourcesDir, certFile);
if (!localFile.exists()) {
try {
localFile.getParentFile().mkdirs();
try (
InputStream in = URI.create(SECURITY_CERTS_URL + certFile).toURL().openStream();
FileOutputStream out = new FileOutputStream(localFile)
) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
} catch (Exception e) {
throw new RuntimeException("Failed to download security certificate: " + certFile, e);
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Security and reliability concerns with certificate download.

  1. Supply chain risk: Certificates are downloaded from a remote URL without integrity verification (no checksums). A compromised GitHub or MITM attack could inject malicious certificates.

  2. Race condition: The !localFile.exists() check followed by download is not atomic. Parallel builds could attempt to download simultaneously, potentially corrupting the file.

  3. Network reliability: Build failures will occur if the network is unavailable or the URL changes.

Consider:

  • Bundling certificates in the repository or a published artifact
  • Adding checksum verification for downloaded files
  • Using file locking for concurrent access
🔒 Suggested improvement with file locking
             File localFile = new File(securityResourcesDir, certFile);
             if (!localFile.exists()) {
+                File lockFile = new File(securityResourcesDir, certFile + ".lock");
                 try {
                     localFile.getParentFile().mkdirs();
+                    // Use file-based locking for concurrent builds
+                    try (java.nio.channels.FileChannel channel = new java.io.RandomAccessFile(lockFile, "rw").getChannel();
+                         java.nio.channels.FileLock lock = channel.lock()) {
+                        if (localFile.exists()) {
+                            continue; // Another process downloaded it
+                        }
                     try (
                         InputStream in = URI.create(SECURITY_CERTS_URL + certFile).toURL().openStream();
                         FileOutputStream out = new FileOutputStream(localFile)
                     ) {
                         // ... download logic ...
                     }
+                    }
                 } catch (Exception e) {
                     throw new RuntimeException("Failed to download security certificate: " + certFile, e);
                 }
             }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
@buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java
around lines 203 - 222, The certificate download loop in TestClustersPlugin (the
for loop iterating SECURITY_CERT_FILES) is insecure and racy: replace the
on-the-fly download logic with a safe strategy — either bundle certificates as
resources/artifacts or, if remote retrieval is required, implement checksum
verification and an atomic, locked download flow: compute/obtain expected
checksums (per cert name), download to a temp file, verify the checksum, then
atomically move/rename into place, and guard concurrent access with a file lock
(or create-exclusive temp file + Files.move ATOMIC_MOVE) to prevent simultaneous
writers; update the RuntimeException message in the download block to include
the cert name and the underlying exception details to aid debugging.

Comment on lines +229 to +232
// Configure each node with certificates and security settings
cluster.getNodes().forEach(node -> {
// Add certificate files
Arrays.stream(SECURITY_CERT_FILES).forEach(cert -> node.extraConfigFile(cert, new File(securityResourcesDir, cert)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find the OpenSearchCluster class and understand getNodes()
find . -name "OpenSearchCluster.java" -type f

Repository: opensearch-project/OpenSearch

Length of output: 155


🏁 Script executed:

# Also search for any class that might define getNodes() for clusters
rg -l "class.*Cluster" --type java | head -20

Repository: opensearch-project/OpenSearch

Length of output: 1791


🏁 Script executed:

# Check the TestClustersPlugin file to understand the full context
rg -n "afterEvaluate" buildSrc/src/main/java/org/opensearch/gradle/testclusters/ --type java -B5 -A10

Repository: opensearch-project/OpenSearch

Length of output: 2140


🏁 Script executed:

# Search for numberOfNodes to understand how nodes are added
rg -n "numberOfNodes" --type java -C3

Repository: opensearch-project/OpenSearch

Length of output: 50385


🏁 Script executed:

# Read the OpenSearchCluster.java file
head -100 buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchCluster.java

Repository: opensearch-project/OpenSearch

Length of output: 3839


🏁 Script executed:

# Look for the getNodes() method and node initialization
rg -n "getNodes|addNode|numberOfNodes" buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchCluster.java -A3 -B3

Repository: opensearch-project/OpenSearch

Length of output: 2235


🏁 Script executed:

# Check how nodes are created in TestClustersPlugin - look at the full configuration context
sed -n '220,250p' buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java

Repository: opensearch-project/OpenSearch

Length of output: 2143


🏁 Script executed:

# Search for node creation/addition patterns in the plugin
rg -n "node|Node" buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java | grep -i "add\|create\|new" | head -20

Repository: opensearch-project/OpenSearch

Length of output: 206


🏁 Script executed:

# Search for setNumberOfNodes usage to understand when it's typically called
rg -n "setNumberOfNodes" --type java -B5 -A2

Repository: opensearch-project/OpenSearch

Length of output: 1003


🏁 Script executed:

# Check if there's documentation or examples of how clusters are configured
find buildSrc -name "*.gradle" -o -name "*.java" | xargs grep -l "setNumberOfNodes" | head -5

Repository: opensearch-project/OpenSearch

Length of output: 153


🏁 Script executed:

# Look for how test clusters are typically defined/configured
rg -n "testClusters" buildSrc/src/main/java/org/opensearch/gradle/testclusters/ -A3 -B3 | head -40

Repository: opensearch-project/OpenSearch

Length of output: 1462


🏁 Script executed:

# Search for actual usage of setNumberOfNodes in build.gradle files
find . -name "*.gradle" -type f -exec grep -l "setNumberOfNodes\|numberOfNodes" {} \; | head -10

Repository: opensearch-project/OpenSearch

Length of output: 462


🏁 Script executed:

# Check one of these build.gradle files to see how testClusters are configured
rg -n "testClusters\|numberOfNodes" -A5 -B5 $(find . -name "*.gradle" -type f | head -3 | tr '\n' ' ')

Repository: opensearch-project/OpenSearch

Length of output: 55


🏁 Script executed:

# Check when the configuration freeze happens in OpenSearchCluster
rg -n "checkFrozen\|configurationFrozen\|freeze" buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchCluster.java -B2 -A2

Repository: opensearch-project/OpenSearch

Length of output: 55


🏁 Script executed:

# Look at the full afterEvaluate block to understand the complete configuration flow
sed -n '135,270p' buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java

Repository: opensearch-project/OpenSearch

Length of output: 7166


🏁 Script executed:

# Search for when freeze() is called on OpenSearchCluster
rg -n "freeze\|freeze()" buildSrc/src/main/java/org/opensearch/gradle/testclusters/ -B3 -A3

Repository: opensearch-project/OpenSearch

Length of output: 55


🏁 Script executed:

# Look for the checkFrozen method to understand the lifecycle
rg -n "checkFrozen" buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchCluster.java -B2 -A5

Repository: opensearch-project/OpenSearch

Length of output: 598


🏁 Script executed:

# Check how test cluster DSL is typically used in actual build.gradle files
head -60 ./qa/smoke-test-multinode/build.gradle

Repository: opensearch-project/OpenSearch

Length of output: 1584


🏁 Script executed:

# Search for where freeze() is called to understand when configuration is locked
rg -n "\.freeze()" buildSrc/src/main/java/org/opensearch/gradle/testclusters/ -B5 -A2

Repository: opensearch-project/OpenSearch

Length of output: 1230


🏁 Script executed:

# Check the task execution flow to understand when configuration is frozen
rg -n "beforeTasks\|afterTasks\|Task\|freeze" buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java | head -30

Repository: opensearch-project/OpenSearch

Length of output: 55


🏁 Script executed:

# Check when claimCluster is called relative to task execution
rg -n "claimCluster" buildSrc/src/main/java/org/opensearch/gradle/testclusters/ -B3 -A3

Repository: opensearch-project/OpenSearch

Length of output: 1899


🏁 Script executed:

# Look for when nodes can be added after configuration
rg -n "setNumberOfNodes\|addNode" buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java

Repository: opensearch-project/OpenSearch

Length of output: 55


🏁 Script executed:

# Check the full lifecycle - when does configuration actually freeze
rg -n "configurationFrozen\|freeze" buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchCluster.java -B5 -A5

Repository: opensearch-project/OpenSearch

Length of output: 55


🏁 Script executed:

# Check if configureEach is applied to newly created nodes
rg -n "configureEach\|allObjects\|whenObjectAdded" buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java -B2 -A3

Repository: opensearch-project/OpenSearch

Length of output: 55


🏁 Script executed:

# Look at line 136 to understand how configureEach is used
sed -n '135,145p' buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java

Repository: opensearch-project/OpenSearch

Length of output: 668


🏁 Script executed:

# Check if there's a way to listen for node additions
rg -n "node\|Node" buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchCluster.java | grep -i "all\|when\|listen" | head -10

Repository: opensearch-project/OpenSearch

Length of output: 55


🏁 Script executed:

# Look at the nodes container more closely to see if there's a better pattern
sed -n '100,160p' buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchCluster.java

Repository: opensearch-project/OpenSearch

Length of output: 2266


🏁 Script executed:

# Check how nodes.all() is used elsewhere - this appears to be the pattern for deferred configuration
rg -n "\.all(" buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchCluster.java -B2 -A2

Repository: opensearch-project/OpenSearch

Length of output: 135


🏁 Script executed:

# Verify the nodes container supports all() method (NamedDomainObjectContainer)
rg -n "NamedDomainObjectContainer" buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchCluster.java

Repository: opensearch-project/OpenSearch

Length of output: 267


🏁 Script executed:

# Check if there are other forEach usages that might have the same issue
rg -n "\.forEach\(" buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java | grep -i node

Repository: opensearch-project/OpenSearch

Length of output: 396


🏁 Script executed:

# Check if setNumberOfNodes is always called before afterEvaluate in actual usage
rg -n "setNumberOfNodes\|numberOfNodes" $(find . -name "build.gradle" -type f) -B2 -A2 | head -50

Repository: opensearch-project/OpenSearch

Length of output: 55


🏁 Script executed:

# Check the Gradle container documentation behavior - does all() apply to future items?
# Let's look at how nodes.all() is used elsewhere in the codebase
rg -n "all\(" buildSrc/src/main/java/org/opensearch/gradle/testclusters/ -B2 -A3

Repository: opensearch-project/OpenSearch

Length of output: 22957


🏁 Script executed:

# Let's verify the exact flow - when is configureSecurityPlugin called vs when can setNumberOfNodes be called
sed -n '310,325p' buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java

Repository: opensearch-project/OpenSearch

Length of output: 928


Use nodes.all() instead of forEach() to ensure dynamically-added nodes receive security configuration.

The forEach() call only applies settings to nodes that exist when the method is executed. If nodes are added later via setNumberOfNodes(), they won't receive the security certificate and settings. Replace the forEach() with all(), which is the established pattern throughout OpenSearchCluster for applying configurations to all nodes (current and future).

Instead of:

cluster.getNodes().forEach(node -> {
    Arrays.stream(SECURITY_CERT_FILES).forEach(cert -> node.extraConfigFile(cert, new File(securityResourcesDir, cert)));
    // ... settings
});

Use:

cluster.getNodes().all(node -> {
    Arrays.stream(SECURITY_CERT_FILES).forEach(cert -> node.extraConfigFile(cert, new File(securityResourcesDir, cert)));
    // ... settings
});
🤖 Prompt for AI Agents
In
@buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java
around lines 229 - 232, Replace the cluster.getNodes().forEach(...) call with
cluster.getNodes().all(...) so security certificate files and settings are
applied to nodes added later (e.g., via setNumberOfNodes()); inside the all(...)
lambda, keep the logic that iterates SECURITY_CERT_FILES and calls
node.extraConfigFile(...) and any other per-node security configuration so every
current and future node receives the certificate and security settings.

@reta
Copy link
Contributor

reta commented Jan 8, 2026

Opened a PR in the job-scheduler to show how these changes in core will simplify the job-scheduler repo: opensearch-project/job-scheduler#873

I am very much unsure about this change (the security plugin was also a stretch but at least it is quite contained), now we essentially have not only bidirectional (binary) dependencies (core <-> plugins), it would also be unfair for plugins outside the OpenSearch organization. I think the OpenSearchIntegTestCase or current test clusters are sufficient at large (the configuration is one time thing).

May be we could come up with security specific Gradle plugin instead? Just an idea.

@cwperks
Copy link
Member Author

cwperks commented Jan 8, 2026

This would greatly simplify integ test setup for many plugins. The security configuration only takes place in 2 scenarios:

  1. opensearch-security is one of the plugins listed in installedPlugins
  2. When passing security.enabled: true

I can remove 2) if that is too much coupling.

Also happy to explore a separate gradle plugin route, but IMO its more sensible within the current TestClusterPlugin.

As always, thank you for reviewing @reta !

@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

❌ Gradle check result for bf72ab7: FAILURE

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change?

@reta
Copy link
Contributor

reta commented Jan 8, 2026

Also happy to explore a separate gradle plugin route, but IMO its more sensible within the current TestClusterPlugin.

I will try to find the time to explore the idea, thanks @cwperks

@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

❌ Gradle check result for bf72ab7: FAILURE

Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change?

@opensearch-trigger-bot
Copy link
Contributor

This PR is stalled because it has been open for 30 days with no activity.

@github-actions
Copy link
Contributor

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit 296f912.

PathLineSeverityDescription
buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java149criticalUse of `groovy.util.Eval.me(installedPluginsProp)` evaluates arbitrary Groovy code supplied via the `-DinstalledPlugins` system property. Any attacker or CI/CD pipeline participant who can set this JVM system property can execute arbitrary code during the Gradle build (e.g., `-DinstalledPlugins="['x'];''.execute('curl ...')"`). This is either a deliberate backdoor or a severe code-injection vulnerability masquerading as a convenience feature.
buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java193highCertificate files (PEM keys, JKS keystores) are downloaded at build time from an external GitHub raw URL (`https://raw.githubusercontent.com/opensearch-project/security/refs/heads/main/...`) with no checksum or signature verification. A compromised repository, a branch-force-push, or a MITM attack could silently substitute malicious certificates, enabling TLS interception of test cluster traffic or injection of trusted rogue CAs into the build environment.
buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java229low`plugins.security.allow_unsafe_democertificates` is explicitly set to `true` on every cluster configured via the security path. While the context is integration testing, hardcoding this flag in a shared plugin means any test cluster that triggers this code path will unconditionally accept demo (insecure) certificates, which could allow test environments to be silently downgraded from proper cert validation without the developer noticing.

The table above displays the top 10 most important findings.

Total: 3 | Critical: 1 | High: 1 | Medium: 0 | Low: 1


Pull Requests Author(s): Please update your Pull Request according to the report above.

Repository Maintainer(s): You can bypass diff analyzer by adding label skip-diff-analyzer after reviewing the changes carefully, then re-run failed actions. To re-enable the analyzer, remove the label, then re-run all actions.


⚠️ Note: The Code-Diff-Analyzer helps protect against potentially harmful code patterns. Please ensure you have thoroughly reviewed the changes beforehand.

Thanks.

Signed-off-by: Craig Perkins <cwperx@amazon.com>
@github-actions
Copy link
Contributor

PR Code Analyzer ❗

AI-powered 'Code-Diff-Analyzer' found issues on commit 6257992.

PathLineSeverityDescription
buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java152criticalgroovy.util.Eval.me(installedPluginsProp) evaluates arbitrary Groovy code from the user-controlled JVM system property '-DinstalledPlugins'. Anyone able to influence build arguments (e.g., CI configuration, local developer invocation, or a compromised pipeline) can achieve arbitrary code execution in the build JVM at configuration time. This is a code injection backdoor disguised as a plugin-list parser.
buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java185highSECURITY_CERTS_URL points to 'refs/heads/main' (a mutable branch, not a pinned commit SHA) on an external GitHub repository. Certificate files are downloaded at build/test time with no integrity check (no checksum or signature verification). If the upstream repository is compromised or the branch is force-pushed, malicious key material could silently replace the test certificates and be injected into running test clusters.
buildSrc/src/main/java/org/opensearch/gradle/testclusters/TestClustersPlugin.java167mediumA Maven repository pointing to 'https://ci.opensearch.org/ci/dbc/snapshots/maven/' is dynamically added to the project during build configuration based on a user-supplied system property. Snapshot repositories are inherently mutable and could serve different artifacts on each resolution. Combined with cacheChangingModulesFor(0, 'seconds') (cache disabled), any snapshot artifact on that server can be swapped at any time, enabling a supply-chain attack against builds that set -DinstalledPlugins.

The table above displays the top 10 most important findings.

Total: 3 | Critical: 1 | High: 1 | Medium: 1 | Low: 0


Pull Requests Author(s): Please update your Pull Request according to the report above.

Repository Maintainer(s): You can bypass diff analyzer by adding label skip-diff-analyzer after reviewing the changes carefully, then re-run failed actions. To re-enable the analyzer, remove the label, then re-run all actions.


⚠️ Note: The Code-Diff-Analyzer helps protect against potentially harmful code patterns. Please ensure you have thoroughly reviewed the changes beforehand.

Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stalled Issues that have stalled

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ensure all plugins in the default distribution have a PR check to run integ tests w/ security

2 participants